import axios from 'axios';
import qs from 'qs';
import lodahMerge from 'lodash/fp/merge';
import Form from '../utils/form';
import { lowerCaseUnderSlashLearChar, pick } from '../utils/helper';

const { CancelToken } = axios;

const REDIRECT_CODE_LIST = [1010, 302, 401];
const OK_CODE_LIST = [1, 200];

const DEFAULT_CONTENT_TYPE = 'application/x-www-form-urlencoded';
axios.defaults.headers.post['Content-Type'] = DEFAULT_CONTENT_TYPE;

function Loading(...rest) {
  console.log(...rest);
}

function makeApi(
  baseUrl,
  apiObj,
  defaultAxiosOptions = {},
  serviceType = 'axios'
) {
  let ctx;
  const stringType = a => Object.prototype.toString.call(a);
  const isPlainObject = s => stringType(s) === '[object Object]';
  const isObjectLike = s => isPlainObject(s) || Array.isArray(s);
  const service = axios.create();
  service.interceptors.request.use(requestInterceptor);

  let lastStatusCode = null;

  const DEFAULT_EXTRA_OPTIONS = {
    showMaskLoading: false,
    showProgressLoading: true,
    showErrorMsg: true,
    resolveWithFullResult: false,
    resolveWithFullResponse: false
  };

  const LAN_MAP = {
    'zh': 'zh-Hans',
    'km': 'km-KH'
  }

  // 请求拦截处理
  function requestInterceptor(config) {
    const headers = {
      ...config.headers,
      lan: LAN_MAP[ctx.store.state.app.language] // 将zh 转换为 'zh-Hans'格式
    };
    // eslint-disable-next-line
    config.headers = headers;
    return config;
  }

  function isFormData(data) {
    return stringType(data) === '[object FormData]';
  }

  function merge(target, source) {
    if (isObjectLike(target) && isObjectLike(source)) {
      const isTargetArray = Array.isArray(target);
      return Object.keys(source).reduce((obj, key) => {
        if (isTargetArray) {
          return [...obj, source[key]];
        }
        return { ...obj, [key]: merge(obj[key], source[key]) };
      }, target);
    }
    if (Array.isArray(source)) {
      return merge([], source);
    }
    if (isPlainObject(source)) {
      return merge({}, source);
    }
    return source;
  }

  function mergeMulti(target, ...sources) {
    return sources.reduce((last, source) => lodahMerge(last, source), target);
  }

  function excludeKeys(object, keys) {
    return Object.keys(object).reduce((o, k) => {
      if (keys.includes(k)) return o;
      return { ...o, [k]: object[k] };
    }, {});
  }

  function splitPath(path) {
    return path.split('/');
  }

  function joinPath(pathArray) {
    return pathArray.join('/');
  }

  function getDryPath(path) {
    return joinPath(splitPath(path).filter(s => !s.match(':')));
  }

  function getUrl(api, query) {
    const pathArray = splitPath(api.path);
    const newPathArray = pathArray.map(s => {
      if (s.match(':')) {
        const key = s.replace(':', '');
        const value = query[key];
        return value;
      }
      return s;
    });
    const newPath = joinPath(newPathArray);
    return `${baseUrl}${newPath}`;
  }

  function removePathKeyFromQuery(api, query) {
    const pathArray = splitPath(api.path);
    const pathKeys = pathArray
      .filter(s => s.match(':'))
      .map(s => s.replace(':', ''));
    if (!isObjectLike(query)) return query;
    pathKeys.forEach(key => {
      // eslint-disable-next-line no-param-reassign
      delete query[key];
    });
    return query;
  }

  function splitParamsData(paramTpl, query) {
    if (isFormData(query)) return { params: {}, data: query };
    if (paramTpl && typeof paramTpl === 'object') {
      const paramsKeys = Object.keys(paramTpl);
      return {
        params: pick(paramsKeys)(query),
        data: excludeKeys(query, paramsKeys)
      };
    }
    return { params: {}, data: query };
  }

  function getLocaleErrorMsg(code, msg) {
    const messages = ctx.i18n.getLocaleMessage(ctx.store.state.app.language);
    const message = messages[code];
    if (message) {
      return message.replace('{MSG}', msg).replace('{CODE}', code);
    }
    return false;
  }

  function removeEmptyField(some) {
    const isEmpty = t => typeof(t) === 'undefined' || t === null ;
    if (isPlainObject(some)) {
      return Object.keys(some).reduce((o, k) => {
        const v = some[k];
        if (!isEmpty(v)) o[k] = v;
        return o;
      }, {});
    }
    return some;
  }

  function getServiceOptions(api, query = {}, extraOptions) {
    const { method = 'get', params, data } = api;
    const serviceOptions = {
      url: getUrl(api, query),
      method,
      ...excludeKeys(api, ['path', 'method', 'params', 'data'])
    };
    const userQuery = removePathKeyFromQuery(api, query);
    const splitedUserQuery = splitParamsData(params, userQuery);

    if (method.toLowerCase() === 'get') {
      serviceOptions.params = removeEmptyField({ ...params, ...userQuery });
    } else {
      serviceOptions.params = removeEmptyField(
        merge(params, splitedUserQuery.params)
      );
      serviceOptions.data = removeEmptyField(
        merge(data, splitedUserQuery.data)
      );
    }
    return mergeMulti({}, defaultAxiosOptions, serviceOptions, extraOptions);
  }

  function responseDataNormalize(data) {
    const code = data.code || data.state;
    const errMsg = data.errorMsg || data.msg;
    const result = data.result || data.data;
    const localeErrorMsg = getLocaleErrorMsg(code, errMsg);
    return { code, errMsg: localeErrorMsg || errMsg, result };
  }

  function dealWithErrorMsg(msg) {
    if (process.client) {
      console.log(msg)
      // ctx.store.commit('showErrorMsg', msg);
    }
  }

  function dealWithResponseData(data, extraOptions, options) {
    const { showErrorMsg, resolveWithFullResult } = extraOptions;
    const normalizedData = responseDataNormalize(data);

    // 获取 response.body 完整信息
    if (resolveWithFullResult) return data;

    const { code, result, errMsg } = normalizedData;

    if (!OK_CODE_LIST.includes(code)) {
      const shouldRedirect = REDIRECT_CODE_LIST.includes(code);
      const isLastCodeRedirect = REDIRECT_CODE_LIST.includes(lastStatusCode);
      if (showErrorMsg && !(shouldRedirect && isLastCodeRedirect)) {
        dealWithErrorMsg(errMsg, options);
      }
      lastStatusCode = code;
      if (shouldRedirect) {
        ctx.store.dispatch('user/clearToken');
      }
      const e = new Error(errMsg);
      Object.keys(data).forEach(k => (e[k] = data[k]));
      throw e;
    }

    return result;
  }

  // 尝试处理网络错误和状态码错误
  function tryInterceptNativeError(e, extraOptions, options) {
    const { showErrorMsg } = extraOptions;
    if (e instanceof Error) {
      const errMsg = e.message;
      if (errMsg) {
        let msg = null;
        if (errMsg.match(/timeout|network/i)) {
          msg = ctx.i18n.t('network_timeout');
        } else if (errMsg.match(/status/i)) {
          msg = ctx.i18n.t('status_error', { CODE: e.response.status });
        } else if (errMsg.match(/tus/i)) {
          msg = ctx.i18n.t('tus_upload_error');
        }
        if (msg) {
          if (showErrorMsg) {
            const getResponseLocaleErrorMsg = () => {
              try {
                const resState = e.response.data.state;
                const localeErrorMsg = getLocaleErrorMsg(resState, msg);
                return localeErrorMsg;
              } catch (err) {
                return '';
              }
            };
            dealWithErrorMsg(getResponseLocaleErrorMsg() || msg, options);
          }
        }
      }
    }
    if (process.server) {
      ctx.error(e.message);
    } else {
      throw e;
    }
  }

  function tryQueryStringifyData(axiosOptions) {
    if (axiosOptions && Form.existChildOf(axiosOptions.data)) {
      // eslint-disable-next-line no-param-reassign
      axiosOptions.data = axiosOptions.data.toFormData();
    }
    if (axiosOptions && axiosOptions.headers && axiosOptions.data) {
      const { headers } = axiosOptions;
      const keys = Object.keys(headers);
      const contentTypeKey = keys.find(k => k.toLowerCase() === 'content-type');
      if (contentTypeKey) {
        const contentTypeValue = headers[contentTypeKey];
        const targetType = 'application/x-www-form-urlencoded';
        const isTargetType = contentTypeValue.toLowerCase().match(targetType);

        if (isTargetType && !isFormData(axiosOptions.data)) {
          // eslint-disable-next-line no-param-reassign
          axiosOptions.data = qs.stringify(axiosOptions.data);
        }
      }
    }
    return axiosOptions;
  }

  async function fetchAndPreprocess(options, extraOptions) {
    const mergedExtraOptions = mergeMulti(
      {},
      DEFAULT_EXTRA_OPTIONS,
      extraOptions
    );
    const { resolveWithFullResponse, showMaskLoading } = mergedExtraOptions;
    let loading = null;
    if (showMaskLoading) {
      loading = Loading({ fullscreen: true });
    }
    try {
      const newOptions = resolveWithFullResponse
        ? { ...options, validateStatus: () => true }
        : options;
      const stringifyOptions = tryQueryStringifyData(newOptions);
      const response = await service(stringifyOptions);
      // 获取 response 完整信息
      if (resolveWithFullResponse) return response;
      return dealWithResponseData(response.data, mergedExtraOptions, options);
    } catch (e) {
      ctx.$sentry.captureException(e);
      return tryInterceptNativeError(e, mergedExtraOptions, options);
    } finally {
      if (loading) loading.close();
    }
  }

  function makeApiInstance(api) {
    return function apiInstance(context, query, extraOptions = {}) {
      ctx = context;
      const serviceOptions = getServiceOptions(api, query, extraOptions);
      const cancelSource = CancelToken.source();
      const promise = fetchAndPreprocess(
        { cancelToken: cancelSource.token, ...serviceOptions },
        extraOptions
      );
      const cancel = () => cancelSource.cancel();
      promise.cancel = cancel;
      promise.abort = cancel;
      return promise;
    };
  }

  function generateApiKey(api) {
    const { path, key } = api;
    if (key) return key;
    const dryPath = getDryPath(path);
    return lowerCaseUnderSlashLearChar(dryPath);
  }

  function transformObjectApiToArray(object) {
    return Object.keys(object).map(key => ({ key, ...object[key] }));
  }

  function reduceApi(api) {
    return {
      [generateApiKey(api)]: makeApiInstance(api)
    };
  }

  if (isPlainObject(apiObj)) {
    return transformObjectApiToArray(apiObj).reduce(
      (o, t) => ({ ...o, ...reduceApi(t) }),
      {}
    );
  }
  if (Array.isArray(apiObj)) {
    return apiObj.reduce((o, t) => ({ ...o, ...reduceApi(t) }), {});
  }
  throw new Error(
    `api doc must be one type of Array, Object. Got ${typeof apiObj}`
  );
}

export function makeBlobUploadApi(apis, CREDENTIALS) {
  function getOptions(options) {
    const FILE_KEY = 'file';
    const SUFFIX = 'jpeg';
    if (options instanceof Blob) {
      return { blob: options, fileKey: FILE_KEY, props: {}, suffix: SUFFIX };
    }
    const { blob, fileKey = FILE_KEY, suffix = SUFFIX, ...props } = options;
    if (!(blob instanceof Blob)) {
      throw new Error(`blob (${blob}) is not a instance of Blob`);
    }
    return { blob, fileKey, props, suffix };
  }

  return Object.keys(apis).reduce(
    (newApis, name) => ({
      ...newApis,
      [name]: apis[name],
      [`${name}Blob`]: function blobUpload(context, options, extraOptions) {
        const { blob, fileKey, suffix, props } = getOptions(options);
        const ms = Date.now();
        const file = new File([blob], `${ms}.${suffix}`, { lastModified: ms });

        const form = new Form();
        Object.keys(CREDENTIALS).forEach(n => {
          form.append(n, CREDENTIALS[n]);
        });
        form.append(fileKey, file);
        Object.keys(props).forEach(n => {
          form.append(n, props[n]);
        });
        return apis[name](context, form.toFormData(), extraOptions);
      }
    }),
    {}
  );
}

export default makeApi;
